#  This is part of the Scopia data visualization example collection
#
#
#  Copyright (C) 2008  Scopia Visual Interfaces Systems 
#                      http://www.scopia.es
#
#  author: Alejandro Conty
#  contributors:
#
#  This program is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program.  If not, see <http://www.gnu.org/licenses/>.
import math
from numpy import *
import labviz
import labviz.bitbroker
import labviz.bitbroker.chart as chart
from scopiavf.font.prerender import PrerenderedText
import random
from bezier import *

class LiveGeom(object):
    def render(self, context, *args): pass

class SectorsGeom(LiveGeom):
    """This class creates and draws the geometry for the "players"
       ring that scales and changes colors with the evolution of the
       accumulated values and size of the transactions"""
    def __init__(self,  players, playertoangle, evolution):
        """Create the basic geometry (quads and lines) for the circle
           from the input parameters:
                   
                   players:        list of player ids involved
                   playertoangle:  a dict that maps every player to a pair of
                                    angles that define the assigned arc in the
                                    circle
                   evolution:       An array of arrays of float32 that assigns
                                    a "shares" status for every player at the
                                    moment of every transaction "transaction" that is
                                    going to be found in the timeline."""
        # minimum angle step for arcs
        anglestep = 2*math.pi / float(Style.circleSteps)
        sectorlist = []
        radiallines = []
        maxpositive = max( evolution.flat )
        minnegative = min( evolution.flat )
        # iterate over players to split the circle between them
        for a in players:
            angle1, angle2 = playertoangle[a]
            # steps for this arc
            steps = max(int((angle2 - angle1) / anglestep), 1)
            step = (angle2 - angle1) / steps
            step2 = step*0.5
            p = zeros( (steps + 1, 3), float32)
            # fill the arc points using the angle range plus step/2 to
            # to ensure there is also the end point (angle2)
            p[:,0] = cos( arange( angle1, angle2+step2, step, float32 ) )
            p[:,1] = sin( arange( angle1, angle2+step2, step, float32 ) )
            # build the quads using the points, one for each step == len(p) - 1
            q = zeros( ((len(p) - 1) * 4, 3), float32 )
            # we leave them with 0 area, cause we will give dimension to them
            # later and at render time
            q[3::4] = q[0::4] = p[:-1]
            q[2::4] = q[1::4] = p[1:]
            # also put the end points for the radial borders, also length 0 by now
            radiallines += [ p[0], p[0], p[-1], p[-1] ]
            sectorlist.append( q )
        numsectors = len(sectorlist)
        # we need to know what sector (player) every quad belongs to
        # so we save it in sectoridx for every quad
        sectoridx = []
        for i, arc in enumerate(sectorlist): sectoridx += [i] * len(arc)
        # convert everything to numpy arrays
        self.sectoridx = array( sectoridx )
        # we concatenate the sector list so we lose the sector grouping
        # but we are ok cause we have that in sectoridx array
        self.sectorlist = concatenate( sectorlist )
        self.radiallines = array( radiallines, float32 )

        # calculate the blending colors for render time
        # we need one for every sector in the circle
        self.coldcolor = array( [(0.0, 0.0, 0.0)] * numsectors, float32)
        self.hotcolordown = array( [(1.0, 0.8, 0.6)] * numsectors, float32)
        self.hotcolorup = array( [(0.6, 0.8, 1)] * numsectors, float32)
        # base radius and amplitude of the variation
        self.radius = array(Style.balanceRadius, float32)
        self.rrange = array( max( maxpositive, -minnegative ), float32)
        # Inner radius is always fixed at this radius, so we set it now
        # and never touch it again
        self.sectorlist[2::4] *= self.radius - Style.radiusAmplitude
        self.sectorlist[3::4] *= self.radius - Style.radiusAmplitude
        # also set inner radius for the radial borders
        self.radiallines[1::2] *= self.radius - Style.radiusAmplitude
        # temporary array to save temporaries at render time
        self.temp = zeros( (len(self.sectorlist) / 4, ), float32 )
        # and we create the final color array to avoid allocation at render time
        self.colors = zeros( (len(self.sectorlist), 3 ), float32 )
        # only for quads cause lines have a fixed color. Outer ring has
        # different "brightness" to get a gradient
        self.colors[0::4] = self.colors[1::4] = (0.8, 0.8, 0.8)
        self.colors[2::4] = self.colors[3::4] = (1.0, 1.0, 1.0)

    def render(self, context, status, delta, deltadir):
        """Update and render the geometry of the circle for the given
           state data:
                    
                    status:     An array with the average "shares" state for
                                every "player" at the current time window
                    delta:      The transactions per time unit ratio at this
                                time window
                    deltadir:   The average direction of the transactions 0.0 means
                                everything going out and 1.0 everything coming in"""
        # this color computing could be optimized to avoid the continuing
        # creation of temporary arrays, but I don't want to obfuscate it
        # even more (yet)
        # deltadir selects which "hot" color to use
        hotcolor = self.hotcolordown*(1.0-deltadir)[...,newaxis] + self.hotcolorup*deltadir[...,newaxis]
        # and delta chooses between cold and that hot color
        sectorcolors = self.coldcolor*(1.0 - delta)[...,newaxis] + hotcolor*delta[...,newaxis]
        # copy colors and multiply them by the choosen colors
        colors = self.colors.copy()
        colors *= sectorcolors[ self.sectoridx ] # sectoridx maps every quad to its sector

        # first copy quads and radial lines
        p = self.sectorlist.copy()
        radlin = self.radiallines.copy()
        # scale the outter radius of the quads using the values in status arg
        # we use rrange to rescale it to our visual range
        # unoptimized exp was: self.radius + status[ self.sectoridx[0::4] ] *radius/self.rrange
        self.temp[:] = status[ self.sectoridx[0::4] ]
        self.temp *= Style.radiusAmplitude / self.rrange
        self.temp += self.radius
        # apply scale to quads (only outter ring points)
        p[0::4] *= self.temp[...,newaxis]
        p[1::4] *= self.temp[...,newaxis]
        # do the same with the radial lines
        temp = status.copy()
        temp *= Style.radiusAmplitude / self.rrange
        temp += self.radius
        radlin[0::4] *= temp[...,newaxis]
        radlin[2::4] *= temp[...,newaxis]
        # send quads to card
        #olddepth = context.depthTest( True )
        context.drawQuadArray( p, colors = colors )
        #context.depthTest( olddepth )
        # now use the same quad points to draw the outter and inner borders
        context.setColor(0.3, 0.3, 0.3)
        context.drawLineArray( p )
        # and finally the radial borders
        context.drawLineArray( radlin )

class TransactionsGeom(LiveGeom):
    """This class creates and draws the geometry for the transactions
       (arcs in the graph)"""  
    def __init__(self, transactions, playertoangle):
        """Takes the transactions (transactions) of the whole timeline and
           creates all the bezier curves in advance so at render time we
           just have to choose and blend the selected ones"""
        # minimum line width and steps
        minarcwidth = 0.001
        mysteps = 15
        # get the biggest transaction in the time line
        maxtransaction = float( max( [ v[chart.V_AMOUNT] for v in transactions] ) )
        # for every curve we will calculate its bezier control points and width
        controlpoints = []
        widths = []
        # I'm sure I could do this with numpy not using a loop, but anyway ...
        for v in transactions:
            afrom = v[chart.V_FROM]
            ato = v[chart.V_TO]
            # first choose a random pair of angles in the origin and target sectors
            # of the circle
            a1 = self.chooseAngle( * playertoangle[afrom] )
            a2 = self.chooseAngle( * playertoangle[ato] )
            # create the four points for the bezier
            controlpoints.append( [( cos(a1)*Style.interiorRadius, sin(a1)*Style.interiorRadius, 0 ),
                                   ( cos(a1)*Style.interiorRadius*0.5, sin(a1)*Style.interiorRadius*0.5, 0 ),
                                   ( cos(a2)*Style.interiorRadius*0.5, sin(a2)*Style.interiorRadius*0.5, 0 ),
                                   ( cos(a2)*Style.interiorRadius, sin(a2)*Style.interiorRadius, 0 ) ] )
            # and the corresponding width depending in the size of the transaction
            widths.append( max(0.01*v[chart.V_AMOUNT]/maxtransaction, minarcwidth) )
        # create all the bezier lines as quad sequences
        self.curves = getMultipleBezierQuads( array( controlpoints, float32 ), array( widths, float32 ), mysteps) 
        # create colors for every point in the curves with a gradient from the start
        # to the end. All the curves have the same sequence of colors whose alpha we
        # will modulate at render time for fading in and out
        self.colors = getMultipleBezierColors( len(controlpoints), mysteps, (1.0, 0.5, 0,   0.1), (0.0, 0.5, 1.0, 0.1) )

    def chooseAngle(self, a1, a2):
        """choose a random position between two angles without getting
           too close to the border"""
        c = (a1 + a2)*0.5
        ran = 2.0*random.random() - 1.0
        radius = (a2 - c) - math.pi*3/180.0
        if radius < 0: radius = 0
        if ran > 0: ran = ran*ran
        else: ran = - ran * ran
        return c + radius*ran

    def render(self, context, current, W):
        """Render only those transactions in "current" list (they are always
           sorted in time) and a "weights" vector (W)."""
        if current:
            # we just need to know the first and last transaction in the
            # time window to choose the right set in our curves array
            first = current[0][chart.V_INDEX]
            last = current[-1][chart.V_INDEX] + 1
            # set alpha channel of our colors using the weights for fading
            self.colors[first:last,:, 3] = W[...,newaxis] * 0.5
            # send it to the graphics card
            context.drawQuadArray( self.curves[first:last].reshape( (-1,3) ) , colors = self.colors[first:last].reshape( (-1,4) ) )



class Style( object ):
    """Style class for rendering all the stuff in bitbroker app. Scene
       nodes call functions in this class with the needed data"""
    # steps for drawing the circle
    circleSteps = 64
    # default radius values and other constants
    interiorRadius = 0.6
    exteriorRadius = 0.8
    balanceRadius = (interiorRadius+exteriorRadius)*0.5
    radiusAmplitude = exteriorRadius - balanceRadius

    def __init__(self, context):
        self.context = context

    def drawPlayers( self, context, node, status, delta, deltadir ):
        """Draws the circle with a sector for each "player" for the
           given state data. It uses SectorsGeom live geometry class.
                    status:     An array with the average "shares" state for
                                every "player" at the current time window
                    delta:      The transactions per time unit ratio at this
                                time window
                    deltadir:   The average direction of the transactions 0.0 means
                                everything going out and 1.0 everything coming in"""
        try: sectorsgeom = node.sectorsgeom
        except:
            # if the live geometry is not available yet we create and save it in the scene node
            sectorsgeom = SectorsGeom(node.chart.players, node.playertoangle, node.chart.evolution)
            node.sectorsgeom = sectorsgeom
        # render the geometry with the given state data
        sectorsgeom.render(context, status, delta, deltadir )
    
    def drawTimeLine( self, context, timeline ):
        """Draws the time line. A rectanlge with a vertical line for every transaction
           that happens at some time."""
        first = timeline.marks[0]
        ran = timeline.marks[-1] - first
        quad = zeros( (4, 3), 'f')
        # first the rectangle
        quad[:2, 1] = 0.05
        quad[2:, 1] = -0.05
        quad[0, 0] = quad[3, 0] = 1.0
        quad[1, 0] = quad[2, 0] = -1.0
        marklines = zeros( (len(timeline.marks)*2, 3), 'f')
        # and then for every "mark" (which is a time value for every transaction
        # we set a vertical line in the line array
        for i in xrange( len(timeline.marks) ):
            marklines[i*2:i*2+2, 0] = 2.0*(timeline.marks[i] - first)/ran - 1.0
            marklines[i*2, 1] = 0.05
            marklines[i*2+1, 1] = -0.05

        context.setColor(0.7, 0.7, 0.7)
        # send the rectangle
        context.drawQuadArray( quad )
        context.setColor(0.0, 0.0, 0.0)
        # send the border of the quad
        context.drawLineStripArray( quad, loop=True )
        context.setColorA(0.0, 0.0, 0.0, 0.3)
        # and finally send the vertical lines
        context.drawLineArray( marklines )
    
    def drawTimeLineWindow( self, context, timeline ):
        """Draws a square over the time line that represents the time
           window where blending for transactions is done."""
        win = zeros( (5, 3), 'f')
        win[:, 2] = 0.2
        win[:,0] = 0 
        win[:2, 1] = 0.06
        win[2:, 1] = -0.06
        win[0, 0] += 1
        win[1:3, 0] -= 1
        win[3, 0] += 1
        win[4] = win[0]
        context.setColorA(1, 1, 1, 0.5)
        context.drawQuadArray( win[:4] )
        context.setColor(0.0, 0.0, 0.0)
        context.drawLineStripArray( win )
    
    def drawTimeLineWindowSizer( self, context, timeline ):
        """Draws the two arrows on both sides of the time window
           that allow the user to resize the time window."""
        pos = 0
        rad = 1
        # just two triangles, we don't even use numpy here cause
        # this is called always from a static node
        context.setColor(0.0, 0.0, 0.0)
        context.startTriangles()
        context.vertex( pos - rad, -0.06, 0.3 )
        context.vertex( pos - rad*0.75, 0, 0.3 )
        context.vertex( pos - rad, 0.06, 0.3 )
        context.vertex( pos + rad, -0.06, 0.3 )
        context.vertex( pos + rad, 0.06, 0.3 )
        context.vertex( pos + rad*0.75, 0, 0.3 )
        context.endTriangles()

    def drawTransactions(self, context, node, current, W):
        """Draws the bezier lines that represent transactions in the choosen
           timewindow (current), fading them in and out using the weights array (W).
           We user TransactionsGeom live geometry class for it."""
        try: transactionsgeom = node.transactionsgeom
        except:
            # if geometry is not available create it
            transactionsgeom = node.transactionsgeom = TransactionsGeom( node.chart.transactions, node.playertoangle )
        # render this time window's transactions
        transactionsgeom.render(context, current, W)

    def old_drawPlayerNames(self, context, node):
        """Draws the text labels for all the "players" in the circle"""
        # we don't do a very efficient drawing of this cause it belongs to a "static"
        # scene node and therefore will be stored in a display list and never called again
        playertoangle = node.playertoangle
        # this is the quad to put text into. Always the same cause we move and scale
        # it using opengl transformations
        points = array( ( (-1, -0.05, 0.0 ), ( 1, -0.05, 0.0 ), ( 1,  0.05, 0.0 ), (-1,  0.05, 0.0 )), 'f' )
        textof = {}
        # create a text geometry object for every "player" that is mapped to the
        # circle in the playertoangle dict
        for player, angles in playertoangle.items():
            textof[player] = PrerenderedText(context, chart.players[player].name, points)
        for player, angles in playertoangle.items():
            # scheme to calculate where this label is going to be placed on screen
            oangle = angle = sum(angles)*0.5
            if angle > math.pi*1.5:
                angle = angle - math.pi*2 - math.pi*0.5
            else:
                angle = angle - math.pi*0.5
            sina= sin(oangle)
            height = 2 * (math.pi-abs(angle)) / math.pi - 1
            height = (height*abs(sina) + sina*(1.0 -abs(sina))) * 0.9
            R = height / sina
            off = 0 #Style.interiorRadius*0.2 * abs(sina)
            end = math.cos(math.asin(height/0.9))*0.9
            if angle > 0:
                end = -end
                off = -off
            # draw two lines that connect the circle sector with the label
            context.setColor(0.7, 0.7, 0.7)
            context.startLineStrip()
            context.vertex( cos(oangle)*Style.interiorRadius*1.02, sin(oangle)*Style.interiorRadius*1.02, -0.1 )
            context.vertex( cos(oangle)*R + off, sin(oangle)*R, -0.1 )
            context.vertex( end, sin(oangle)*R, -0.1 )
            context.endLineStrip()
            # and now draw the label itself
            context.setColor(0.5, 0.5, 0.5)
            context.saveTransform()
            # calculate and set the needed translation and scale transform
            scale = min( 9 * (angles[1] - angles[0]) / (math.pi*0.5), 0.5 )
            if angle < 0: pos = end + scale*(-textof[player].minX)
            else: pos = end - scale*textof[player].maxX
            context.translate( (pos, sin(oangle)*R, -0.1 ) )
            context.scale ( (scale, scale, scale) ) 
            # draw the text
            textof[player].draw(context)
            context.loadTransform()
    
    def drawPlayerNames(self, context, node):
        """Draws the text labels for all the "players" in the circle"""
        playertoangle = node.playertoangle
        textof = {}
        context.setColor(0.3, 0.3, 0.3)
        for player, angles in playertoangle.items():
            acenter = sum(angles) * 0.5
            radius = min(angles[1] - acenter, 1.5*math.pi/180.0)
            a1, a2 = acenter - radius, acenter + radius
            points = zeros( (4,3), float32 )
            off = array( (cos(acenter), sin(acenter), 0), float32 ) * 0.5
            if acenter < math.pi*0.5 or acenter > math.pi*1.5:
                points[:2] = (cos(a1), sin(a1), 0)
                points[2:] = (cos(a2), sin(a2), 0)
                points *= Style.exteriorRadius
                points[1:3] += off
                text = PrerenderedText(context, chart.players[player].name, points, align = 'left')
            else:
                points[:2] = (cos(a2), sin(a2), 0)
                points[2:] = (cos(a1), sin(a1), 0)
                points *= Style.exteriorRadius
                points[0] += off
                points[3] += off
                text = PrerenderedText(context, chart.players[player].name, points, align = 'right')
            text.draw(context)

def getMultipleBezierColors( numcurves, numsteps, color1, color2 ):
    """Returns an array of sequences of colors for a curves array. The
       sequences of colors perform a gradient from color1 to color2 and
       from the start of the curve to the end. Assuming that the curves
       are built using quads (four points for each step). The same
       color sequence is repeated for every curve (numcurves). """
    t = arange( 0, 1.0 + 0.5/numsteps, 1.0/numsteps, float32)
    omt = 1.0 - t
    colors = zeros( (numcurves, numsteps*4, 4), float32 )
    color1, color2 = array(color1, float32), array(color2, float32)
    colors[:,0::4] = colors[:,1::4] = t[:-1,newaxis] * color2 + omt[:-1,newaxis] * color1
    colors[:,2::4] = colors[:,3::4] = t[1:,newaxis] * color2 + omt[1:,newaxis] * color1
    return colors
